package com.spring.demo.domain;

import java.io.Serializable;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.spring.demo.authenticator.PoEntityAuditorAware;
import com.spring.demo.repository.jpa.JpaBusinessRepository;

@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public abstract class AbstractPoEntityManager<ID extends Serializable, PO extends PoEntity<ID>, Repository extends JpaBusinessRepository<PO, ID>, T> {

	private static final Logger LOG = LoggerFactory.getLogger(AbstractPoEntityManager.class);

	private ID id;

	private PO instance;
	
	private Repository repository;

	@Autowired
	private ApplicationContext applicationContext;
	
	@PersistenceContext
	private EntityManager entityManager;

	
	private PoEntityAuditorAware auditorAware;
	
	public void setInstance(PO instance) {
		if (instance == null) {
			this.instance = null;
			this.id = null;
		} else {
			this.instance = instance;
			this.id = instance.getId();
		}
	}
	
	public PO getInstance() {
		return instance;
	}
	
	@Transactional
	public PO save() {
		Assert.notNull(instance, "Required instance must not be null");
		this.preSave();
		if (instance.getId() == null) {
			touchForCreate(instance);
		}
		touchForUpdate(instance);
		this.setInstance(repository.save(instance));
		this.postSave();
		return instance;
	}
	
	protected void postSave() {
		
	}

	protected void preSave() {
		
	}
	
	@Transactional
	public void remove() {
		Assert.notNull(instance, "Required instance must not be null");
		Assert.notNull(id, "Required instance's id must not be null");
		this.preRemove();
		repository.delete(instance);
		this.postRemove();
		this.setInstance(null);
	}

	@Transactional
	public void removeById(ID id) {
		Assert.notNull(id, "Required id must not be null");
		instance = this.findOneById(id);
		if (instance != null) {
			this.remove();
		}
	}
	
	@Transactional(readOnly = true)
	public PO findOne() {
		Assert.notNull(id, "Required id must not be null");
		return findOneById(id);
	}

	@Transactional(readOnly = true)
	public PO findOneById(ID id) {
		this.id = id;
		instance = repository.findOne(id);
		return instance;
	}

	protected void postRemove() {
	}

	protected void preRemove() {
	}
	
	public void touchForCreate(Object target) {
		touch(target, true);
	}

	public void touchForUpdate(Object target) {
		touch(target, false);
	}
	
	private void touch(Object target, boolean isNew) {
		if (!(target instanceof AuditablePoEntity)) {
			return;
		}

		@SuppressWarnings("unchecked")
		AuditablePoEntity<ID> auditable = (AuditablePoEntity<ID>) target;

		User auditor = touchAuditor(auditable, isNew);
		DateTime now = touchDate(auditable, isNew);

		Object defaultedNow = now == null ? "not set" : now;
		Object defaultedAuditor = auditor == null ? "unknown" : auditor;

		LOG.debug("Touched {} - Last modification at {} by {}", new Object[] { auditable, defaultedNow,
				defaultedAuditor });
	}
	
	private User touchAuditor(final AuditablePoEntity<ID> auditable, boolean isNew) {
		if (null == auditorAware) {
			return null;
		}
		User auditor = auditorAware.getCurrentAuditor();
		String auditorId = auditorAware.getCurrentAuditorId();
		if (isNew) {
			auditable.setCreatedBy(auditorId);
		}
		auditable.setLastModifiedBy(auditorId);
		return auditor;
	}

	private DateTime touchDate(final AuditablePoEntity<ID> auditable, boolean isNew) {
		DateTime now = new DateTime();
		if (isNew) {
			auditable.setCreatedDate(now);
		}
		auditable.setLastModifiedDate(now);
		return now;
	}
	
	
	public void setId(ID id) {
		this.id = id;
	}

	public ID getId() {
		return id;
	}
	
	public void setApplicationContext(ApplicationContext applicationContext) {
		this.applicationContext = applicationContext;
	}

	public ApplicationContext getApplicationContext() {
		return applicationContext;
	}
	
	@Autowired
	public void setAuditorAware(PoEntityAuditorAware auditorAware) {
		this.auditorAware = auditorAware;
	}

	public PoEntityAuditorAware getAuditorAware() {
		return auditorAware;
	}

	public EntityManager getEntityManager() {
		return entityManager;
	}

	public void setEntityManager(EntityManager entityManager) {
		this.entityManager = entityManager;
	}

	public Repository getRepository() {
		return repository;
	}

	@Autowired
	public void setRepository(Repository repository) {
		this.repository = repository;
	}
}
